iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0
Software Development

以MicroPython在ESP32上實作Insulin Delivery Service系列 第 30 篇

Day 30 - 取回 History Data (2) - IDD RACP & IDD History Data

  • 分享至 

  • xImage
  •  

昨天已經知道如何由 BTree 取出資料了,那麼就來完成最後一哩路吧!

1. 實作 Report Stored Records

基本上,IDD Record Access Control Point 和先前的 IDD Status Reader Control Point 與 IDD Command Control Point 幾乎一樣,所以若不支援 Abort 指令的話,可以直接照抄其他的 Control Point。不過因咱們會以支援 Abort 指令為前提,所以雖然還是繼承了 BaseCP,但其邏輯實現會有些不一樣。

首先實現 IDD Record Access Control Point 的建構函數:

class IddRACP(BaseCP):
    def __init__(self, history_mgr: HistoryManager):
        super().__init__(0x2B27)

        self._history_mgr = history_mgr
        self._fn_on_opcode = self._on_opcode

        # 指示是否在處理歷史資料
        self._is_transmitting = False

        # 指示是否收到 Abort 指令
        self._is_aborting = False

        # 指示是否先前的動作已停止
        self._is_aborted = asyncio.Event()

然後咱們也必須覆寫 BaseCP 的 _check_att_error():

def _check_att_error(self, data: bytes) -> int:
    att_err = super()._check_att_error(data)

    # IDS 並沒有說明若前一個指令是以下情況時怎麼處理:
    #   Abort
    #   Delete
    #   Report Number of Stored Records
    # 目前實作:
    #   若指令不會造成 _is_transmitting 為 True,
    #   或 abort 已經在執行,
    #   則 ATT Error 都為 Procedure Already In Progress

    # 返回 ATT Eroor,如果:
    # ATT Error 不是 Procedure Already In Progress
    # 或 不是 Abort 指令
    # 或 目前沒有在處理 History Data
    # 或 abort 已經在執行
    if att_err and (
        att_err != ATT_ERR_PROCEDURE_ALREADY_IN_PROGRESS
        or data[0] != _ABORT
        or not self._is_transmitting
        or self._is_aborting
    ):
        return att_err

    return 0

接著實現 _parse_write_data():

def _parse_write_data(self, data: bytes):
    opcode = data[0]
    operator = data[1]
    data_mv = memoryview(data)
    operand = self._get_operand(data_mv)

    # 與 IDD Status Reader Control Point 和 IDD Command Control Point 的流程不同,
    # 先檢查 operator 和 operand 是否正確。
    # 因為 IDD RACP 有 Abort,
    # 若留到協程運作時才檢查,
    # 可能導致接踵而來的 Abort 無法正確判定,
    # 雖然在正常流程下,
    # 理應不該發生
    if opcode == _REPORT_RECORDS or opcode == _REPORT_NUMBER:
        if self._check_rec_range(operator, operand) == _SUCCESS:
            self._is_transmitting = True
            self._is_aborting = False
            self._is_aborted.clear()

    elif opcode == _ABORT:
        if self._check_abort_params(operator, operand) == _SUCCESS:
            self._is_aborting = True

    ble.global_var.is_cp_in_progress = True
    micropython.schedule(self._fn_on_opcode, data_mv)

這裡使用到的 _check_rec_range() 只是檢查 operator 和 operand 是否符合規定的內容,便由看官自由發揮吧~
(Report Stored Records 的指令格式請參考昨天的內容)

然後咱們用 _on_opcode() 根據 Op Code 來分發給協程處理:

def _on_opcode(self, data: bytes):
    opcode = data[0]
    operator = data[1]
    operand = self._get_operand(data)

    if opcode == _REPORT_RECORDS:
        asyncio.create_task(self._on_report_records(operator, operand))

    elif opcode == _REPORT_NUMBER:
        asyncio.create_task(self._on_report_number(operator, operand))

    elif opcode == _ABORT:
        asyncio.create_task(self._on_abort(operator, operand))

    else:
        asyncio.create_task(self._on_not_supported_opcode(opcode))

要注意, _on_opcode() 是在 _parse_write_data() 裡,以 _fn_on_opcode 為參數,間接傳進 micropython.schedule()。

接下來就是重頭戲了!在 _on_report_records() 裡,會定義 2 個內部函數:

  • send_record()
    因 RACP 會一次取得許多 History Data,所以此函數負責每次發送一筆事件,告知訂閱者要傳送 History Data。
  • _respond()
    當所有 History Data 傳送完畢,必須傳送 Response Code 給 GATT Client,告知傳送結束。
async def _on_report_records(self, operator: int, operand: bytes):
    async def send_record(rec: bytes):
        common.eventbus.publish(core.events.EVENT_HISTORY_SENDING, rec)

        # 暫停一會兒,以讓資料可以傳送。
        # 因協程是單執行緒,若無此,將等到所有資料都放進 queue 後才有機會傳送。
        await asyncio.sleep_ms(10)

    def _respond(count: int):
        self._respond_error(
            _REPORT_RECORDS, _SUCCESS if count else _NO_RECORDS_FOUND
        )

    await self._handle_record_work(
        _REPORT_RECORDS, operator, operand, send_record, _respond
    )

而在 _handle_record_work() 裡,會從 operator 和 operand 取得對應的 filter 和 History Data:

async def _handle_record_work(
    self, opcode: int, operator: int, operand: bytes, fn_item_async, fn_final
):
    ...

    if operator == _FIRST:
        rec = self._history_mgr.get_first_history()

    elif operator == _LAST:
        rec = self._history_mgr.get_last_history()

    if self._check_abort_and_set():
        return

    if rec is not None:
        if fn_item_async is not None:
            await fn_item_async(rec)

        record_count += 1
    else:
        ...

        for rec in self._history_mgr.get_histories(filter_min, filter_max):
            if self._check_abort_and_set():
                return

            if fn_item_async is not None:
                await fn_item_async(rec)

            record_count += 1

    fn_final(record_count)

那麼 Report Stored Records 就完成了,最後就剩實際送出 History Data。

2. IDD History Data

還記得在 IddRACP._on_report_records() 裡,咱們讓它每接收一筆 History Data,便發送一次 EVENT_HISTORY_SENDING 嗎?

咱們讓 IDD History Data 來處理這件事:

class IddHistoryData(ble.mixin.NotifyMixin, ble.stack.Characteristic):
    def __init__(self):
        ble.stack.Characteristic.__init__(self, 0x2B28, notify=True)
        ble.mixin.NotifyMixin.__init__(self)

        common.eventbus.subscribe(
            core.events.EVENT_HISTORY_SENDING, self._on_history_sending
        )

    def _build_notify_payload(self, buf: bytearray | memoryview, arg: bytes) -> int:
        data_len = len(arg)
        buf[:data_len] = arg
        return data_len

    def _on_history_sending(self, history: bytes):
        ble.stack.BleTxScheduler().add(
            ble.stack.ACT_NOTIFY, self.send_data, self.value_handle, history
        )
  • IddHistoryData 訂閱事件 EVENT_HISTORY_SENDING 後,只是將收到的資料以 Notify 發送出去。
  • _build_notify_payload() 則只是單純地將資料複製到緩衝區。

如此一來,當咱們將這些 Characteristic 加入到 IDS Server 裡後,便可用相關指令來傳送 History Data。


至此,咱們的 IDS Server 便宣告完成了~
。:.゚ヽ(*´∀`)ノ゚.:。


上一篇
Day 29 - 取回 History Data (1)
下一篇
Day 31 - 不要一直重複要求配對阿!
系列文
以MicroPython在ESP32上實作Insulin Delivery Service 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言